/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.myfaces.test.webapp.impl;

import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.WebClient;

import org.apache.myfaces.test.webapp.api.ResourceFinder;
import org.apache.myfaces.test.webapp.api.tester.WebappTester;
import org.apache.myfaces.test.webapp.impl.metadata.TestCaseMetadata;
import org.apache.myfaces.test.webapp.impl.metadata.action.TestActionClick;
import org.apache.myfaces.test.webapp.impl.metadata.action.TestActionEvent;
import org.apache.myfaces.test.webapp.impl.metadata.action.TestActionGetPage;
import org.apache.myfaces.test.webapp.impl.metadata.action.TestActionInput;
import org.apache.myfaces.test.webapp.impl.metadata.assertion.*;
import org.jboss.shrinkwrap.api.Archive;

import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseId;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
 * This class represents the default Tester that must be instantiated by any test class
 * using the {@link org.apache.myfaces.test.webapp.api.annotation.Tester} annotation.
 * The class provides all necessary methods to control the test case.
 *
 * @author Cosmin Martinconi, 01.07.2010
 */
public class WebappTesterImpl implements WebappTester {

    private static final Logger log = Logger.getLogger(WebappTesterImpl.class.getName());
    private static final String LOCAL_NAME = "http://localhost:8080/";
    private static final String ARCHIVE_NAME = "test";

    private FacesContext facesContext;
    private UIViewRoot root;

    private List<TestCaseMetadata> testMetadataList;
    private WebClient webClient;


    public void initializeWebappTester() {

        ProxyInterceptor proxyInterceptor = (ProxyInterceptor) ResourceFinder.getResource(ProxyInterceptor.class.getName());
        
        facesContext = (FacesContext)proxyInterceptor.newInstance(FacesContext.class);
        root = (UIViewRoot)proxyInterceptor.newInstance(UIViewRoot.class);
    }

    public static WebappTesterImpl getCurrentInstance() {
        return (WebappTesterImpl) ResourceFinder.getResource(ResourceFinder.WEBAPP_TESTER);
    }

    public Archive<?> createArchive(Archive<?> applicationArchive) {
        ArchiveManager archiveManager = ArchiveManager.getCurrentInstance();
        archiveManager.createArchive(applicationArchive, ARCHIVE_NAME);

        if (archiveManager.isTestSuiteConfig())
            return archiveManager.getTestSuiteArchive();
        else
            return archiveManager.getTestArchive();
    }

    public void executeTestMethod() {

        WebappTestPhaseListener listener = new WebappTestPhaseListener();
        listener.registerToLifecycle();

        Page page = null;

        for (TestCaseMetadata tcm : testMetadataList) {
            listener.setRequestAssertList(tcm.getAssertionList());

            for (TestActionEvent tae : tcm.getActionEventList()) {
                page = tae.doAction(page);
            }
        }

        TesttErrorManager.getCurrentInstance().handleErrors();
    }

    public void setUpTestMethod() {

        testMetadataList = new ArrayList<TestCaseMetadata>();

        if (webClient == null)
            webClient = new WebClient();

        AnnotationManager annotationManager = (AnnotationManager) ResourceFinder.getResource(AnnotationManager.class.getName());

        String startURL = LOCAL_NAME + ARCHIVE_NAME + "/" + annotationManager.getView().id();
        TestActionGetPage actionGetPage = new TestActionGetPage(startURL, webClient);

        addNewMetadata().addActionEvent(actionGetPage);
    }

    public void tearDownTestMethod() {
        testMetadataList = null;
        webClient.closeAllWindows();
    }

    public FacesContext getFacesContext() {
        return facesContext;
    }
    
    public UIViewRoot getRoot() {
        return root;
    }

    private TestCaseMetadata getCurrentMetadata() {
        return testMetadataList.get(testMetadataList.size() - 1);
    }

    private TestCaseMetadata addNewMetadata() {
        TestCaseMetadata newMetadata = new TestCaseMetadata();
        testMetadataList.add(newMetadata);

        return newMetadata;
    }

    /*--------------------------------------------------------------
     Begin methods, that interact like the user would do
    ---------------------------------------------------------------*/

    /**
     * Inputs the given value in the field specified by the in() method.
     * @param value
     * @return
     */
    public Input input(String value) {
        TestActionInput actionInput = new TestActionInput();
        actionInput.setInputValue(value);
        getCurrentMetadata().addActionEvent(actionInput);
        
        return new InputImpl(actionInput);
    }

    /**
     * Clicks the UICommand component with the given clientId
     *
     * @param clientId
     * @throws java.io.IOException
     */
    public void click(String clientId) {
        TestActionClick actionClick = new TestActionClick();
        actionClick.setClientId(clientId);

        addNewMetadata().addActionEvent(actionClick);
    }


    /*--------------------------------------------------------------
     Begin methods, that verify expected behavior or values
    ---------------------------------------------------------------*/

    public Assertable assertThat(Object injectedAssertableValue) {

        TestAssertCondition assertCondition = new TestAssertCondition();

        if ((AssertableValue.class).isAssignableFrom(injectedAssertableValue.getClass())) {
            assertCondition.setAssertableValue((AssertableValue) injectedAssertableValue);
        } else {
            throw new IllegalArgumentException("No " + AssertableValue.class.getName() + " instance found!");
        }

        getCurrentMetadata().addAssertion(assertCondition);

        return new AssertableImpl(assertCondition);
    }

    public Call expectCall(Object injectedAssertableValue) {

        TestAssertExpectCall assertExpectCall = new TestAssertExpectCall();

        if ((AssertableValue.class).isAssignableFrom(injectedAssertableValue.getClass())) {
            assertExpectCall.setAssertableValue((AssertableValue) injectedAssertableValue);
        } else {
            throw new IllegalArgumentException("No " + AssertableValue.class.getName() + " instance found!");
        }        

        getCurrentMetadata().addAssertion(assertExpectCall);

        return new CallImpl(assertExpectCall);
    }

    public final Call expectLastCall() {
        TestAssertExpectCall assertExpectLastCall = new TestAssertExpectCall();
        AssertableProxyMethod proxyMethod = ProxyInterceptor.getCurrentInstance().getSavedMethod();

        assertExpectLastCall.setAssertableValue(proxyMethod);
        getCurrentMetadata().addAssertion(assertExpectLastCall);

        return new CallImpl(assertExpectLastCall);
    }

    /*--------------------------------------------------------------
     Begin classes needed for fluent API
    ---------------------------------------------------------------*/

    public final class AssertableImpl implements Assertable{

        private TestAssertCondition assertCondition;

        public AssertableImpl(TestAssertCondition assertCondition) {
            this.assertCondition = assertCondition;
        }

        public PhaseAware is(String expectedValue) {
            assertCondition.setExpectedValue(expectedValue);

            return new PhaseAwareImpl(assertCondition);
        }
    }

    public final class PhaseAwareImpl implements PhaseAware {

        private TestAssertCondition assertCondition;

        public PhaseAwareImpl(TestAssertCondition assertCondition) {
            this.assertCondition = assertCondition;
        }

        public void after(PhaseId phase) {
            assertCondition.setPhaseId(phase);
            assertCondition.setAfter(true);
        }

        public void before(PhaseId phase) {
            assertCondition.setPhaseId(phase);
            assertCondition.setAfter(false);
        }
    }

    public final class InputImpl implements Input {

        private TestActionInput actionInput;

        public InputImpl(TestActionInput actionInput) {
            this.actionInput = actionInput;
        }

        public void into(String clientId) {
            actionInput.setClientId(clientId);
        }
    }

    public final class CallImpl implements Call {

        private TestAssertExpectCall assertExpectCall;

        public CallImpl(TestAssertExpectCall assertExpectCall) {
            this.assertExpectCall = assertExpectCall;
        }

        public void in(PhaseId phase) {
            assertExpectCall.setPhaseId(phase);
            assertExpectCall.setAfter(false);
        }
    }

}
